package pl.s7n.paint;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEditSupport;

import pl.s7n.paint.commands.AbstractStrokeCmd;
import pl.s7n.paint.commands.CmdFactory;
import pl.s7n.paint.commands.FloodFillStrokeCmd;
import pl.s7n.paint.edits.DrawingEditManager;
import pl.s7n.paint.edits.ResizeEditManager;
import pl.s7n.paint.utils.Redrawable;
import pl.s7n.paint.utils.SelfUpdatable;
import pl.s7n.paint.utils.Utils;

/**
 * The most important class here. Manages all the drawing stuff, mouse handling, <br>
 * supports undo/redo actions for drawing shapes, resizing and transforming image.
 * 
 * @author 7
 */
public class DrawableCanvas extends JPanel implements Redrawable {
  private static final long serialVersionUID    = 6521792967019433325L;

  // Undo support:
  UndoableEditSupport       undoableEditSupport = new UndoableEditSupport(this);
  DrawingEditManager        drawingEditManager  = new DrawingEditManager(undoableEditSupport, this);
  ResizeEditManager         resizeEditManager   = new ResizeEditManager(undoableEditSupport, this);

  // Current, not yet committed drawing action.
  AbstractStrokeCmd         action;

  // Currently tools:
  DrawingToolInfo           tool;

  // Underlying image instance
  BufferedImage             image;

  // size of the grey empty border above & on the left of displayed image
  public static final int   CANVAS_EMPTY_BORDER = 10;

  /**
   * Set up default tools, create a clear image with given params, bind mouse listeners.
   * 
   * @param w
   *          new image width
   * @param h
   *          new image height
   */
  public DrawableCanvas(int w, int h) {
    tool = Utils.defaultTool();
    image = Utils.defaultImage(w, h);

    MouseAdapter ml = new DrawingMouseAdapter();
    addMouseListener(ml);
    addMouseMotionListener(ml);

    setBackground(Color.LIGHT_GRAY);

    Utils.setFixedSize(this, w + CANVAS_EMPTY_BORDER, h + CANVAS_EMPTY_BORDER);
  }

  /*
   * Decorator for undoable edit support
   */
  public void addUndoableEditListener(UndoableEditListener listener) {
    undoableEditSupport.addUndoableEditListener(listener);
  }

  /**
   * Paints only the image itself, plus the current uncommitted action if exitsts<br>
   * (so this action is not rendered into the image, but as overlay)
   */
  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2 = (Graphics2D) g;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

    g2.translate(CANVAS_EMPTY_BORDER, CANVAS_EMPTY_BORDER);
    g2.drawRenderedImage(image, null);

    // draw current action as overlay
    g2.setClip(0, 0, image.getWidth(), image.getHeight());
    if (action != null) {
      action.draw(g2);
    }
  }

  /**
   * Handling mouse moves and corresponding actions of the currently selected tool.<br>
   * <ul>
   * <li>On mouse pressed an appropriate command object is built and initialized.
   * <li>On mouse dragged the current action command is updated with freshly harvested mouse info :)
   * <li>On mouse released the action is being finished, so an appropriate edit event is delivered<br>
   * to the undo manager. This way the action is 'committed' to the image, and no longer is <br>
   * displayed as a overlay. Current action reference is nullified.
   * 
   * @author 7
   */
  private class DrawingMouseAdapter extends MouseAdapter {

    @Override
    public void mousePressed(MouseEvent e) {
      super.mousePressed(e);

      if (contains(e.getPoint())) {

        Point transformed = Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER);

        action = CmdFactory.createCmd(tool.tool);
        action.setTool(tool.clone());
        switch (tool.tool) {
          case LINE:
          case ELLIPSE:
          case RECT:
          case FILL_ELLIPSE:
          case FILL_RECT:
            action.setStart(transformed);
            break;
          case PENCIL:
          case BRUSH:
          case ERASER:
            action.setStart(transformed);
            Path2D path = new Path2D.Double();
            path.moveTo(transformed.x, transformed.y);
            action.setShape(path);
            break;
          case FILL:
            break;
        }
      }
    }

    @Override
    public void mouseDragged(MouseEvent e) {
      super.mouseDragged(e);

      if (action != null) {
        Point transformed = Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER);
        switch (tool.tool) {
          case LINE:
            action.setShape(Utils.createLine(action.getStart(), transformed));
            break;
          case PENCIL:
          case BRUSH:
          case ERASER:
            ((Path2D) action.getShape()).lineTo(transformed.x, transformed.y);
            break;
          case ELLIPSE:
          case FILL_ELLIPSE:
            action.setShape(Utils.createEllipse(action.getStart(), transformed));
            break;
          case RECT:
          case FILL_RECT:
            action.setShape(Utils.createRectangle(action.getStart(), transformed));
            break;
        }
        // no need to repaint if dragging FILL
        if (tool.tool != DrawingTools.FILL) {
          repaint();
        }
      }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
      super.mouseReleased(e);
      if (action != null) {
        try {
          switch (tool.tool) {
            case LINE:
            case ELLIPSE:
            case FILL_ELLIPSE:
            case RECT:
            case FILL_RECT:
              break;
            case PENCIL:
            case BRUSH:
            case ERASER:
              ((SelfUpdatable) action).update();
              break;
            case FILL:
              Point transformed = Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER);
              FloodFillStrokeCmd ff = (FloodFillStrokeCmd) action;
              ff.setStart(transformed);
              ff.setCurrentImageRef(image);
              break;
            default:
              return;
          }

          drawingEditManager.editPerformed(action);

          Graphics2D g2 = image.createGraphics();
          g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
          g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
          action.draw(g2);

          repaint();
        } finally {
          action = null;
        }
      }
    }
  };

  /**
   * Heavyweight redraw. Redraws the whole image (not only component). <br>
   * Starts with clearing the image. Then draws the first image layer <br>
   * (the last background image snapshot from resize & transform edit manager). <br>
   * Finally paints the actions stored @ drawing manager - these are not images, <br>
   * but simply instructions how to draw and what to draw (Drawing commands).
   */
  @Override
  public void redraw() {
    Graphics2D g2 = Utils.clearImage(image);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

    resizeEditManager.draw(g2);
    drawingEditManager.draw(g2);

    repaint();
  }

  /**
   * @return true if the image was edited.
   */
  public boolean wasEdited() {
    return drawingEditManager.hasActions() || resizeEditManager.hasActions();
  }

  public BufferedImage getImage() {
    return image;
  }

  public void setImage(BufferedImage image) {
    setImage(image, true);
  }

  /**
   * Sets the current image to draw on.
   * 
   * @param image
   * @param reset
   *          if reset is true, the undo managers are zeroed back to defaults.
   */
  public void setImage(BufferedImage image, boolean reset) {
    if (reset) {
      drawingEditManager.reset();
      resizeEditManager.reset();
    }

    this.image = image;
    redraw();
  }

  public DrawingToolInfo getTool() {
    return tool;
  }

  public void setTool(DrawingToolInfo tool) {
    this.tool = tool;
  }

  /**
   * @return true if the image canvas contains this point.
   */
  public boolean contains(Point in) {
    return in.x >= CANVAS_EMPTY_BORDER //
        && in.y >= CANVAS_EMPTY_BORDER //
        && in.x < image.getWidth() + CANVAS_EMPTY_BORDER //
        && in.y < image.getHeight() + CANVAS_EMPTY_BORDER;
  }

  /**
   * Returns translated coordinates of Point over this component, as String. (for coord status bar)
   */
  public String getReadableCoords(Point in) {
    if (in.x >= CANVAS_EMPTY_BORDER //
        && in.y >= CANVAS_EMPTY_BORDER //
        && in.x < image.getWidth() + CANVAS_EMPTY_BORDER //
        && in.y < image.getHeight() + CANVAS_EMPTY_BORDER) {

      StringBuilder sb = new StringBuilder("[ ") //
          .append(in.x + 1 - CANVAS_EMPTY_BORDER) //
          .append(" ; ").append(in.y + 1 - CANVAS_EMPTY_BORDER).append(" ]");
      return sb.toString();
    }
    return " ";
  }

  public DrawingEditManager getDrawingEditManager() {
    return drawingEditManager;
  }

  public ResizeEditManager getResizeEditManager() {
    return resizeEditManager;
  }
}
